diff --git a/spellcheck.dic b/spellcheck.dic
index e9f7eec22f6..faa855921b4 100644
--- a/spellcheck.dic
+++ b/spellcheck.dic
@@ -1,4 +1,4 @@
-299
+300
 &
 +
 <
@@ -263,6 +263,7 @@ tokio
 Tokio
 tokio's
 Tokio's
+tty
 tuple
 Tuple
 tx
diff --git a/tokio/src/io/blocking.rs b/tokio/src/io/blocking.rs
index 1af5065456d..4d366c85f72 100644
--- a/tokio/src/io/blocking.rs
+++ b/tokio/src/io/blocking.rs
@@ -50,6 +50,13 @@ cfg_io_blocking! {
     }
 }
 
+impl<T> Blocking<T> {
+    #[cfg_attr(not(feature = "io-std"), allow(dead_code))]
+    pub(crate) fn inner(&self) -> Option<&T> {
+        self.inner.as_ref()
+    }
+}
+
 impl<T> AsyncRead for Blocking<T>
 where
     T: Read + Unpin + Send + 'static,
diff --git a/tokio/src/io/stderr.rs b/tokio/src/io/stderr.rs
index 0988e2d9da0..6c82d141b75 100644
--- a/tokio/src/io/stderr.rs
+++ b/tokio/src/io/stderr.rs
@@ -3,6 +3,7 @@ use crate::io::stdio_common::SplitByUtf8BoundaryIfWindows;
 use crate::io::AsyncWrite;
 
 use std::io;
+use std::io::IsTerminal;
 use std::pin::Pin;
 use std::task::Context;
 use std::task::Poll;
@@ -132,3 +133,14 @@ impl AsyncWrite for Stderr {
         Pin::new(&mut self.std).poll_shutdown(cx)
     }
 }
+
+impl Stderr {
+    /// Returns true if the descriptor/handle refers to a terminal/tty.
+    pub fn is_terminal(&self) -> bool {
+        self.std
+            .inner()
+            .inner()
+            .map(|stderr| stderr.is_terminal())
+            .unwrap_or_default()
+    }
+}
diff --git a/tokio/src/io/stdin.rs b/tokio/src/io/stdin.rs
index 877c48b30fb..1031745f51e 100644
--- a/tokio/src/io/stdin.rs
+++ b/tokio/src/io/stdin.rs
@@ -1,7 +1,7 @@
 use crate::io::blocking::Blocking;
 use crate::io::{AsyncRead, ReadBuf};
 
-use std::io;
+use std::io::{self, IsTerminal};
 use std::pin::Pin;
 use std::task::Context;
 use std::task::Poll;
@@ -96,3 +96,13 @@ impl AsyncRead for Stdin {
         Pin::new(&mut self.std).poll_read(cx, buf)
     }
 }
+
+impl Stdin {
+    /// Returns true if the descriptor/handle refers to a terminal/tty.
+    pub fn is_terminal(&self) -> bool {
+        self.std
+            .inner()
+            .map(|stdin| stdin.is_terminal())
+            .unwrap_or_default()
+    }
+}
diff --git a/tokio/src/io/stdio_common.rs b/tokio/src/io/stdio_common.rs
index 4adbfe23606..2d8064d27fc 100644
--- a/tokio/src/io/stdio_common.rs
+++ b/tokio/src/io/stdio_common.rs
@@ -17,6 +17,10 @@ impl<W> SplitByUtf8BoundaryIfWindows<W> {
     pub(crate) fn new(inner: W) -> Self {
         Self { inner }
     }
+
+    pub(crate) fn inner(&self) -> &W {
+        &self.inner
+    }
 }
 
 // this constant is defined by Unicode standard.
diff --git a/tokio/src/io/stdout.rs b/tokio/src/io/stdout.rs
index f46ca0f05c4..0d779999b13 100644
--- a/tokio/src/io/stdout.rs
+++ b/tokio/src/io/stdout.rs
@@ -2,6 +2,7 @@ use crate::io::blocking::Blocking;
 use crate::io::stdio_common::SplitByUtf8BoundaryIfWindows;
 use crate::io::AsyncWrite;
 use std::io;
+use std::io::IsTerminal;
 use std::pin::Pin;
 use std::task::Context;
 use std::task::Poll;
@@ -181,3 +182,14 @@ impl AsyncWrite for Stdout {
         Pin::new(&mut self.std).poll_shutdown(cx)
     }
 }
+
+impl Stdout {
+    /// Returns true if the descriptor/handle refers to a terminal/tty.
+    pub fn is_terminal(&self) -> bool {
+        self.std
+            .inner()
+            .inner()
+            .map(|stdout| stdout.is_terminal())
+            .unwrap_or_default()
+    }
+}
diff --git a/tokio/src/process/mod.rs b/tokio/src/process/mod.rs
index 565795ac4e6..ecd0d39201f 100644
--- a/tokio/src/process/mod.rs
+++ b/tokio/src/process/mod.rs
@@ -1381,6 +1381,11 @@ impl ChildStdin {
             inner: imp::stdio(inner)?,
         })
     }
+
+    /// Returns true if the descriptor/handle refers to a terminal/tty.
+    pub fn is_terminal(&self) -> bool {
+        self.inner.is_terminal()
+    }
 }
 
 impl ChildStdout {
@@ -1396,6 +1401,11 @@ impl ChildStdout {
             inner: imp::stdio(inner)?,
         })
     }
+
+    /// Returns true if the descriptor/handle refers to a terminal/tty.
+    pub fn is_terminal(&self) -> bool {
+        self.inner.is_terminal()
+    }
 }
 
 impl ChildStderr {
@@ -1411,6 +1421,11 @@ impl ChildStderr {
             inner: imp::stdio(inner)?,
         })
     }
+
+    /// Returns true if the descriptor/handle refers to a terminal/tty.
+    pub fn is_terminal(&self) -> bool {
+        self.inner.is_terminal()
+    }
 }
 
 impl AsyncWrite for ChildStdin {
diff --git a/tokio/src/process/unix/mod.rs b/tokio/src/process/unix/mod.rs
index c9d1035f53d..4203a620522 100644
--- a/tokio/src/process/unix/mod.rs
+++ b/tokio/src/process/unix/mod.rs
@@ -41,7 +41,7 @@ use mio::unix::SourceFd;
 use std::fmt;
 use std::fs::File;
 use std::future::Future;
-use std::io;
+use std::io::{self, IsTerminal};
 use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
 use std::pin::Pin;
 use std::process::{Child as StdChild, ExitStatus, Stdio};
@@ -279,6 +279,10 @@ impl ChildStdio {
     pub(super) fn into_owned_fd(self) -> io::Result<OwnedFd> {
         convert_to_blocking_file(self).map(OwnedFd::from)
     }
+
+    pub(crate) fn is_terminal(&self) -> bool {
+        self.as_fd().is_terminal()
+    }
 }
 
 impl fmt::Debug for ChildStdio {
diff --git a/tokio/src/process/windows.rs b/tokio/src/process/windows.rs
index db3c15790ce..c06a3fc4c90 100644
--- a/tokio/src/process/windows.rs
+++ b/tokio/src/process/windows.rs
@@ -24,7 +24,10 @@ use std::fmt;
 use std::fs::File as StdFile;
 use std::future::Future;
 use std::io;
-use std::os::windows::prelude::{AsRawHandle, IntoRawHandle, OwnedHandle, RawHandle};
+use std::io::IsTerminal;
+use std::os::windows::prelude::{
+    AsHandle, AsRawHandle, BorrowedHandle, IntoRawHandle, OwnedHandle, RawHandle,
+};
 use std::pin::Pin;
 use std::process::Stdio;
 use std::process::{Child as StdChild, Command as StdCommand, ExitStatus};
@@ -199,6 +202,10 @@ impl ChildStdio {
     pub(super) fn into_owned_handle(self) -> io::Result<OwnedHandle> {
         convert_to_file(self).map(OwnedHandle::from)
     }
+
+    pub(crate) fn is_terminal(&self) -> bool {
+        self.as_handle().is_terminal()
+    }
 }
 
 impl AsRawHandle for ChildStdio {
@@ -207,6 +214,12 @@ impl AsRawHandle for ChildStdio {
     }
 }
 
+impl AsHandle for ChildStdio {
+    fn as_handle(&self) -> BorrowedHandle<'_> {
+        self.raw.as_handle()
+    }
+}
+
 impl AsyncRead for ChildStdio {
     fn poll_read(
         mut self: Pin<&mut Self>,