diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md
index 819882bbb68..39c3bd8abc5 100644
--- a/docs/UPGRADING.md
+++ b/docs/UPGRADING.md
@@ -7,6 +7,48 @@ Here you can find a list of migration guides to handle breaking changes between
 Configuration file lookup in current working directory and its parents is dropped. The command line flag `--config-file`
 must be specified to use an alternative configuration file from the one in the data directory.
 
+### Command `outdated` output change
+
+For `text` format (default), the command prints now a single table for platforms and libraries instead of two separate
+tables.
+
+Similarly, for JSON and YAML formats, the command prints now a single valid object, with `platform` and `libraries`
+top-level keys. For example, for JSON output:
+
+```
+$ arduino-cli outdated --format json
+{
+  "platforms": [
+    {
+      "id": "arduino:avr",
+      "installed": "1.6.3",
+      "latest": "1.8.6",
+      "name": "Arduino AVR Boards",
+      ...
+    }
+  ],
+  "libraries": [
+    {
+      "library": {
+        "name": "USBHost",
+        "author": "Arduino",
+        "maintainer": "Arduino \u003cinfo@arduino.cc\u003e",
+        "category": "Device Control",
+        "version": "1.0.0",
+        ...
+      },
+      "release": {
+        "author": "Arduino",
+        "version": "1.0.5",
+        "maintainer": "Arduino \u003cinfo@arduino.cc\u003e",
+        "category": "Device Control",
+        ...
+      }
+    }
+  ]
+}
+```
+
 ## 0.31.0
 
 ### Added `post_install` script support for tools
diff --git a/internal/cli/core/list.go b/internal/cli/core/list.go
index b53a75e25b1..316b4275a73 100644
--- a/internal/cli/core/list.go
+++ b/internal/cli/core/list.go
@@ -52,8 +52,14 @@ func runListCommand(args []string, all bool, updatableOnly bool) {
 	List(inst, all, updatableOnly)
 }
 
-// List print a list of installed platforms.
+// List gets and prints a list of installed platforms.
 func List(inst *rpc.Instance, all bool, updatableOnly bool) {
+	platforms := GetList(inst, all, updatableOnly)
+	feedback.PrintResult(installedResult{platforms})
+}
+
+// GetList returns a list of installed platforms.
+func GetList(inst *rpc.Instance, all bool, updatableOnly bool) []*rpc.Platform {
 	platforms, err := core.GetPlatforms(&rpc.PlatformListRequest{
 		Instance:      inst,
 		UpdatableOnly: updatableOnly,
@@ -62,8 +68,7 @@ func List(inst *rpc.Instance, all bool, updatableOnly bool) {
 	if err != nil {
 		feedback.Fatal(tr("Error listing platforms: %v", err), feedback.ErrGeneric)
 	}
-
-	feedback.PrintResult(installedResult{platforms})
+	return platforms
 }
 
 // output from this command requires special formatting, let's create a dedicated
diff --git a/internal/cli/lib/list.go b/internal/cli/lib/list.go
index 3e65cddcd65..57674c38a23 100644
--- a/internal/cli/lib/list.go
+++ b/internal/cli/lib/list.go
@@ -60,8 +60,23 @@ func runListCommand(args []string, all bool, updatable bool) {
 	List(instance, args, all, updatable)
 }
 
-// List lists all the installed libraries.
+// List gets and prints a list of installed libraries.
 func List(instance *rpc.Instance, args []string, all bool, updatable bool) {
+	installedLibs := GetList(instance, args, all, updatable)
+	feedback.PrintResult(installedResult{
+		onlyUpdates:   updatable,
+		installedLibs: installedLibs,
+	})
+	logrus.Info("Done")
+}
+
+// GetList returns a list of installed libraries.
+func GetList(
+	instance *rpc.Instance,
+	args []string,
+	all bool,
+	updatable bool,
+) []*rpc.InstalledLibrary {
 	name := ""
 	if len(args) > 0 {
 		name = args[0]
@@ -95,11 +110,7 @@ func List(instance *rpc.Instance, args []string, all bool, updatable bool) {
 		libs = []*rpc.InstalledLibrary{}
 	}
 
-	feedback.PrintResult(installedResult{
-		onlyUpdates:   updatable,
-		installedLibs: libs,
-	})
-	logrus.Info("Done")
+	return libs
 }
 
 // output from this command requires special formatting, let's create a dedicated
diff --git a/internal/cli/outdated/outdated.go b/internal/cli/outdated/outdated.go
index 35dfab911da..8a56f21b36a 100644
--- a/internal/cli/outdated/outdated.go
+++ b/internal/cli/outdated/outdated.go
@@ -16,13 +16,18 @@
 package outdated
 
 import (
+	"fmt"
 	"os"
+	"sort"
+	"strings"
 
 	"github.com/arduino/arduino-cli/i18n"
 	"github.com/arduino/arduino-cli/internal/cli/core"
+	"github.com/arduino/arduino-cli/internal/cli/feedback"
 	"github.com/arduino/arduino-cli/internal/cli/instance"
 	"github.com/arduino/arduino-cli/internal/cli/lib"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
+	"github.com/arduino/arduino-cli/table"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
@@ -49,8 +54,97 @@ func runOutdatedCommand(cmd *cobra.Command, args []string) {
 	Outdated(inst)
 }
 
-// Outdated returns a list of outdated platforms and libraries
+// Outdated prints a list of outdated platforms and libraries
 func Outdated(inst *rpc.Instance) {
-	core.List(inst, false, true)
-	lib.List(inst, []string{}, false, true)
+	feedback.PrintResult(
+		outdatedResult{core.GetList(inst, false, true), lib.GetList(inst, []string{}, false, true)},
+	)
+}
+
+// output from this command requires special formatting, let's create a dedicated
+// feedback.Result implementation
+type outdatedResult struct {
+	Platforms     []*rpc.Platform         `json:"platforms,omitempty"`
+	InstalledLibs []*rpc.InstalledLibrary `json:"libraries,omitempty"`
+}
+
+func (ir outdatedResult) Data() interface{} {
+	return &ir
+}
+
+func (ir outdatedResult) String() string {
+	if len(ir.Platforms) == 0 && len(ir.InstalledLibs) == 0 {
+		return tr("No outdated platforms or libraries found.")
+	}
+
+	// A table useful both for platforms and libraries, where some of the fields will be blank.
+	t := table.New()
+	t.SetHeader(
+		tr("ID"),
+		tr("Name"),
+		tr("Installed"),
+		tr("Latest"),
+		tr("Location"),
+		tr("Description"),
+	)
+	t.SetColumnWidthMode(2, table.Average)
+	t.SetColumnWidthMode(3, table.Average)
+	t.SetColumnWidthMode(5, table.Average)
+
+	// Based on internal/cli/core/list.go
+	for _, p := range ir.Platforms {
+		name := p.Name
+		if p.Deprecated {
+			name = fmt.Sprintf("[%s] %s", tr("DEPRECATED"), name)
+		}
+		t.AddRow(p.Id, name, p.Installed, p.Latest, "", "")
+	}
+
+	// Based on internal/cli/lib/list.go
+	sort.Slice(ir.InstalledLibs, func(i, j int) bool {
+		return strings.ToLower(
+			ir.InstalledLibs[i].Library.Name,
+		) < strings.ToLower(
+			ir.InstalledLibs[j].Library.Name,
+		) ||
+			strings.ToLower(
+				ir.InstalledLibs[i].Library.ContainerPlatform,
+			) < strings.ToLower(
+				ir.InstalledLibs[j].Library.ContainerPlatform,
+			)
+	})
+	lastName := ""
+	for _, libMeta := range ir.InstalledLibs {
+		lib := libMeta.GetLibrary()
+		name := lib.Name
+		if name == lastName {
+			name = ` "`
+		} else {
+			lastName = name
+		}
+
+		location := lib.GetLocation().String()
+		if lib.ContainerPlatform != "" {
+			location = lib.GetContainerPlatform()
+		}
+
+		available := ""
+		sentence := ""
+		if libMeta.GetRelease() != nil {
+			available = libMeta.GetRelease().GetVersion()
+			sentence = lib.Sentence
+		}
+
+		if available == "" {
+			available = "-"
+		}
+		if sentence == "" {
+			sentence = "-"
+		} else if len(sentence) > 40 {
+			sentence = sentence[:37] + "..."
+		}
+		t.AddRow("", name, lib.Version, available, location, sentence)
+	}
+
+	return t.Render()
 }
diff --git a/internal/integrationtest/outdated/outdated_test.go b/internal/integrationtest/outdated/outdated_test.go
index 64e1f39304e..77f7c54bcd4 100644
--- a/internal/integrationtest/outdated/outdated_test.go
+++ b/internal/integrationtest/outdated/outdated_test.go
@@ -53,7 +53,7 @@ func TestOutdated(t *testing.T) {
 		lines[i] = strings.TrimSpace(lines[i])
 	}
 	require.Contains(t, lines[1], "Arduino AVR Boards")
-	require.Contains(t, lines[4], "USBHost")
+	require.Contains(t, lines[2], "USBHost")
 }
 
 func TestOutdatedUsingLibraryWithInvalidVersion(t *testing.T) {
diff --git a/internal/integrationtest/update/update_test.go b/internal/integrationtest/update/update_test.go
index 8bfe7a46c49..927ae0e3c02 100644
--- a/internal/integrationtest/update/update_test.go
+++ b/internal/integrationtest/update/update_test.go
@@ -67,7 +67,7 @@ func TestUpdateShowingOutdated(t *testing.T) {
 	require.Contains(t, lines[0], "Downloading index: library_index.tar.bz2 downloaded")
 	require.Contains(t, lines[1], "Downloading index: package_index.tar.bz2 downloaded")
 	require.Contains(t, lines[3], "Arduino AVR Boards")
-	require.Contains(t, lines[6], "USBHost")
+	require.Contains(t, lines[4], "USBHost")
 }
 
 func TestUpdateWithUrlNotFound(t *testing.T) {
diff --git a/internal/integrationtest/upgrade/upgrade_test.go b/internal/integrationtest/upgrade/upgrade_test.go
index ffd8ecbe8e7..0b041a9bcc2 100644
--- a/internal/integrationtest/upgrade/upgrade_test.go
+++ b/internal/integrationtest/upgrade/upgrade_test.go
@@ -50,7 +50,7 @@ func TestUpgrade(t *testing.T) {
 	require.NoError(t, err)
 	lines := strings.Split(string(stdout), "\n")
 	require.Contains(t, lines[1], "Arduino AVR Boards")
-	require.Contains(t, lines[4], "USBHost")
+	require.Contains(t, lines[2], "USBHost")
 
 	_, _, err = cli.Run("upgrade")
 	require.NoError(t, err)
@@ -58,7 +58,7 @@ func TestUpgrade(t *testing.T) {
 	// Verifies cores and libraries have been updated
 	stdout, _, err = cli.Run("outdated")
 	require.NoError(t, err)
-	require.Contains(t, string(stdout), "No libraries update is available.")
+	require.Contains(t, string(stdout), "No outdated platforms or libraries found.")
 }
 
 func TestUpgradeUsingLibraryWithInvalidVersion(t *testing.T) {