diff --git a/core/modules/media/config/install/system.action.media_update_metadata.yml b/core/modules/media/config/install/system.action.media_update_metadata.yml
new file mode 100644
index 0000000000..e31298e408
--- /dev/null
+++ b/core/modules/media/config/install/system.action.media_update_metadata.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+id: media_update_metadata
+label: 'Update metadata'
+type: media
+plugin: media_update_metadata
+configuration: {  }
diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml
index a419dd866e..e056c5e6a9 100644
--- a/core/modules/media/config/schema/media.schema.yml
+++ b/core/modules/media/config/schema/media.schema.yml
@@ -124,3 +124,6 @@ filter_settings.media_embed:
       sequence:
         type: string
         label: 'View mode'
+action.configuration.media_update_metadata:
+  type: action_configuration_default
+  label: 'Update metadata'
diff --git a/core/modules/media/media.links.contextual.yml b/core/modules/media/media.links.contextual.yml
index 1945ef5001..cf919f396c 100644
--- a/core/modules/media/media.links.contextual.yml
+++ b/core/modules/media/media.links.contextual.yml
@@ -3,6 +3,12 @@ entity.media.edit_form:
   group: media
   title: Edit
 
+media.update_metadata:
+  route_name: media.update_metadata
+  group: media
+  title: 'Update metadata'
+  weight: 5
+
 entity.media.delete_form:
   route_name: entity.media.delete_form
   group: media
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index 8c90b3bfff..f34287db0a 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -528,3 +528,20 @@ function media_views_query_substitutions(ViewExecutable $view) {
     '***ADMINISTER_MEDIA***' => (int) $account->hasPermission('administer media'),
   ];
 }
+
+/**
+ * Implements hook_entity_operation().
+ */
+function media_entity_operation(EntityInterface $entity) {
+  $operations = [];
+  if ($entity->getEntityTypeId() === 'media' && $entity->access('update')) {
+    $operations['update_metadata'] = [
+      'title' => t('Update metadata'),
+      'url' => new Url('media.update_metadata', [
+        'media' => $entity->id(),
+      ]),
+      'weight' => 150,
+    ];
+  }
+  return $operations;
+}
diff --git a/core/modules/media/media.post_update.php b/core/modules/media/media.post_update.php
index f69e078686..8babd5e420 100644
--- a/core/modules/media/media.post_update.php
+++ b/core/modules/media/media.post_update.php
@@ -5,6 +5,8 @@
  * Post update functions for Media.
  */
 
+use Drupal\system\Entity\Action;
+
 /**
  * Implements hook_removed_post_updates().
  */
@@ -16,3 +18,19 @@ function media_removed_post_updates() {
     'media_post_update_add_status_extra_filter' => '9.0.0',
   ];
 }
+
+/**
+ * Install the 'Update metadata' action.
+ */
+function media_post_update_install_update_metadata_action() {
+  if (!Action::load('media_update_metadata')) {
+    Action::create([
+      'id' => 'media_update_metadata',
+      'label' => 'Update metadata',
+      'type' => 'media',
+      'plugin' => 'media_update_metadata',
+    ])
+      ->save();
+  }
+
+}
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
index 4c8d851774..338dfd9064 100644
--- a/core/modules/media/media.routing.yml
+++ b/core/modules/media/media.routing.yml
@@ -13,6 +13,13 @@ entity.media.revision:
     _access_media_revision: 'view'
     media: \d+
 
+media.update_metadata:
+  path: '/media/{media}/update-metadata'
+  defaults:
+    _controller: '\Drupal\media\Controller\UpdateMetadataController::updateMetadata'
+  requirements:
+    _entity_access: 'media.update'
+
 media.oembed_iframe:
   path: '/media/oembed'
   defaults:
diff --git a/core/modules/media/src/Controller/UpdateMetadataController.php b/core/modules/media/src/Controller/UpdateMetadataController.php
new file mode 100644
index 0000000000..5d22490497
--- /dev/null
+++ b/core/modules/media/src/Controller/UpdateMetadataController.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\media\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
+use Drupal\media\MediaInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Controller that triggers metadata updates on a given media item.
+ *
+ * @internal
+ *   Controllers are internal.
+ */
+class UpdateMetadataController extends ControllerBase {
+
+  /**
+   * Updates metadata on a media entity and reloads the page.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   The media item for which to update metadata
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response to return the user to the page they were on.
+   */
+  public function updateMetadata(MediaInterface $media) {
+    $media->updateMetadata();
+    $this->entityTypeManager()->getStorage('media')->save($media);
+    $this->messenger()->addStatus($this->t('Updated metadata on media item %label', [
+      '%label' => $media->label(),
+    ]));
+    return new RedirectResponse($this->reloadPage());
+  }
+
+  /**
+   * Reloads the previous page or return to the media overview.
+   */
+  protected function reloadPage() {
+    if (\Drupal::request()->query->has('destination')) {
+      return $this->getRedirectDestination()->get();
+    }
+    return Url::fromRoute('entity.media.collection')->toString();
+  }
+
+}
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index 6649cccfd0..c8d817d807 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -368,29 +368,63 @@ public function prepareSave() {
         ->loadUnchanged($id);
     }
 
-    $media_source = $this->getSource();
-    foreach ($this->translations as $langcode => $data) {
-      if ($this->hasTranslation($langcode)) {
-        $translation = $this->getTranslation($langcode);
-        // Try to set fields provided by the media source and mapped in
-        // media type config.
-        foreach ($translation->bundle->entity->getFieldMap() as $metadata_attribute_name => $entity_field_name) {
-          // Only save value in entity field if empty. Do not overwrite existing
-          // data.
-          if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $translation->hasSourceFieldChanged())) {
-            $translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name));
-          }
-        }
+    foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
+      $translation = $this->getTranslation($langcode);
 
-        // Try to set a default name for this media item if no name is provided.
-        if ($translation->get('name')->isEmpty()) {
-          $translation->setName($translation->getName());
-        }
+      // Set fields provided by the media source and mapped in the media type
+      // config.
+      $this->updateMappedMetadata($translation);
 
-        // Set thumbnail.
-        if ($translation->shouldUpdateThumbnail($this->isNew())) {
-          $translation->updateThumbnail();
-        }
+      // Try to set a default name for this media item if no name is provided.
+      if ($translation->get('name')->isEmpty()) {
+        $translation->setName($translation->getName());
+      }
+
+      // Set thumbnail.
+      if ($translation->shouldUpdateThumbnail($this->isNew())) {
+        $translation->updateThumbnail();
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateMetadata() {
+    foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
+      $translation = $this->getTranslation($langcode);
+      // Update the mapped metadata, even if non-empty values exist.
+      $this->updateMappedMetadata($translation, TRUE);
+      $translation->updateThumbnail();
+    }
+
+    return $this;
+  }
+
+  /**
+   * Maps metadata values into entity field values.
+   *
+   * @param \Drupal\media\MediaInterface $translation
+   *   The media translation we are updating.
+   * @param bool $overwrite_existing
+   *   (optional) If TRUE, metadata values will always be copied into mapped
+   *   field values. If FALSE, values will be copied only if the mapped field is
+   *   empty or if the media source field changed. Defaults to FALSE.
+   */
+  protected function updateMappedMetadata(MediaInterface $translation, $overwrite_existing = FALSE) {
+    $media_source = $this->getSource();
+    foreach ($translation->bundle->entity->getFieldMap() as $metadata_attribute_name => $entity_field_name) {
+      if (!$translation->hasField($entity_field_name)) {
+        continue;
+      }
+      /**
+       * Populate the field value in one of these scenarios:
+       * - The caller of this function asked for it explicitly.
+       * - The entity field is empty.
+       * - The media source field has changed.
+       */
+      if ($overwrite_existing || $translation->get($entity_field_name)->isEmpty() || $translation->hasSourceFieldChanged()) {
+        $translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name));
       }
     }
   }
diff --git a/core/modules/media/src/MediaInterface.php b/core/modules/media/src/MediaInterface.php
index 3aeaef56ab..6e292a1724 100644
--- a/core/modules/media/src/MediaInterface.php
+++ b/core/modules/media/src/MediaInterface.php
@@ -64,4 +64,18 @@ public function setCreatedTime($timestamp);
    */
   public function getSource();
 
+  /**
+   * Update the media entity's field values from the source's metadata.
+   *
+   * This will fetch metadata from the source field, and update all values that
+   * are mapped to entity fields. This will overwrite non-empty existing values,
+   * and will update all translations of the entity. This will also try to
+   * update the entity's thumbnail with a newer version, if available.
+   *
+   * @return \Drupal\media\MediaInterface
+   *   An unsaved version of this media item, after updating the mapped field
+   *   items and the thumbnail.
+   */
+  public function updateMetadata();
+
 }
diff --git a/core/modules/media/src/Plugin/Action/UpdateMetadataAction.php b/core/modules/media/src/Plugin/Action/UpdateMetadataAction.php
new file mode 100644
index 0000000000..a5669acefe
--- /dev/null
+++ b/core/modules/media/src/Plugin/Action/UpdateMetadataAction.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\media\Plugin\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Updates metadata (including the thumbnail) of a media entity.
+ *
+ * @Action(
+ *   id = "media_update_metadata",
+ *   action_label = @Translation("Update metadata")
+ * )
+ */
+class UpdateMetadataAction extends ActionBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The media entity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $mediaStorage;
+
+  /**
+   * Creates a new UpdateMetadataAction plugin instance.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The entity storage.
+   * @param array $configuration
+   *   The plugin configuration array.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->mediaStorage = $entity_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $container->get('entity_type.manager')->getStorage('media'),
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    /** @var \Drupal\media\MediaInterface $entity */
+    $entity->updateMetadata();
+    $this->mediaStorage->save($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    /** @var \Drupal\Core\Entity\EntityInterface $object */
+    $result = $object->access('update', $account, TRUE);
+    return $return_as_object ? $result : $result->isAllowed();
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/MediaBulkFormTest.php b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php
index d65f34e2b0..a6006777d2 100644
--- a/core/modules/media/tests/src/Functional/MediaBulkFormTest.php
+++ b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php
@@ -72,12 +72,14 @@ public function testBulkForm() {
 
     // Check the operations are accessible to the logged in user.
     $this->drupalGet('test-media-bulk-form');
-    // Current available actions: Delete, Save, Publish, Unpublish.
+    // Current available actions: Delete, Save, Publish, Unpublish, Update
+    // Metadata.
     $available_actions = [
       'media_delete_action',
       'media_publish_action',
       'media_save_action',
       'media_unpublish_action',
+      'media_update_metadata',
     ];
     foreach ($available_actions as $action_name) {
       $assert_session->optionExists('action', $action_name);
diff --git a/core/modules/media/tests/src/Functional/MediaContextualLinksTest.php b/core/modules/media/tests/src/Functional/MediaContextualLinksTest.php
deleted file mode 100644
index f5cf9a81bc..0000000000
--- a/core/modules/media/tests/src/Functional/MediaContextualLinksTest.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-namespace Drupal\Tests\media\Functional;
-
-use Drupal\media\Entity\Media;
-
-/**
- * Tests views contextual links on media items.
- *
- * @group media
- */
-class MediaContextualLinksTest extends MediaFunctionalTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = [
-    'contextual',
-  ];
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $defaultTheme = 'stark';
-
-  /**
-   * Tests contextual links.
-   */
-  public function testMediaContextualLinks() {
-    \Drupal::configFactory()
-      ->getEditable('media.settings')
-      ->set('standalone_url', TRUE)
-      ->save(TRUE);
-
-    $this->container->get('router.builder')->rebuild();
-
-    // Create a media type.
-    $mediaType = $this->createMediaType('test');
-
-    // Create a media item.
-    $media = Media::create([
-      'bundle' => $mediaType->id(),
-      'name' => 'Unnamed',
-    ]);
-    $media->save();
-
-    $user = $this->drupalCreateUser([
-      'administer media',
-      'access contextual links',
-    ]);
-    $this->drupalLogin($user);
-
-    $this->drupalGet('media/' . $media->id());
-    $this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'media:media=' . $media->id() . ':');
-  }
-
-}
diff --git a/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php b/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php
index 646d4d2f10..4bd5284524 100644
--- a/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php
+++ b/core/modules/media/tests/src/Functional/MediaOverviewPageTest.php
@@ -139,6 +139,9 @@ public function testMediaOverviewPage() {
     $delete_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.delete a', $row1);
     $this->assertSame('Delete', $delete_link1->getText());
     $assert_session->linkByHrefExists('/media/' . $media1->id() . '/delete');
+    $update_metadata_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.update-metadata a', $row1);
+    $this->assertSame('Update metadata', $update_metadata_link1->getText());
+    $assert_session->linkByHrefExists('/media/' . $media1->id() . '/update-metadata');
 
     // Make the user the owner of the unpublished media item and assert the
     // media item is only visible with the 'view own unpublished media'
diff --git a/core/modules/media/tests/src/Functional/MediaUpdateMetadataControllerTest.php b/core/modules/media/tests/src/Functional/MediaUpdateMetadataControllerTest.php
new file mode 100644
index 0000000000..a691dc2825
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/MediaUpdateMetadataControllerTest.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\file\Entity\File as FileEntity
+use Drupal\media\Entity\Media;
+use Drupal\media\Plugin\media\Source\File;
+
+/**
+ * Tests the media "Update Metadata" controller.
+ *
+ * @group media
+ */
+class MediaUpdateMetadataControllerTest extends MediaFunctionalTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Test that by going to the update metadata page, we update the metadata.
+   */
+  public function testUpdateMetadataController() {
+    $assert_session = $this->assertSession();
+
+    $media_type = $this->createMediaType('file');
+    $source_plugin = $media_type->getSource();
+
+    // Initially the "name" metadata is the filename.
+    $file = FileEntity::create([
+      'uri' => 'public://foo.txt',
+      'uid' => 1,
+    ]);
+    $file->setPermanent();
+    $file->save();
+
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = Media::create([
+      'bundle' => $media_type->id(),
+      'uid' => 1,
+      'field_media_file' => [
+        'target_id' => $file->id(),
+      ],
+    ]);
+    $media->save();
+
+    $name_metadata = $source_plugin->getMetadata($media, File::METADATA_ATTRIBUTE_NAME);
+    $this->assertSame('foo.txt', $name_metadata);
+
+    // Rename the file, simulating a remote metadata change.
+    $file_id = $source_plugin->getSourceFieldValue($media);
+    /** @var \Drupal\file\FileInterface $file */
+    $file = $this->container->get('entity_type.manager')
+      ->getStorage('file')
+      ->load($file_id);
+    $file->setFilename('bar.txt');
+    $file->save();
+
+    // Go to the update metadata page and verify the controller triggered an
+    // update.
+    $this->drupalGet("/media/{$media->id()}/update-metadata");
+    $assert_session->statusCodeEquals(200);
+
+    $name_metadata = $source_plugin->getMetadata($media, File::METADATA_ATTRIBUTE_NAME);
+    $this->assertSame('bar.txt', $name_metadata);
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaContextualLinksTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaContextualLinksTest.php
new file mode 100644
index 0000000000..d9619f520a
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaContextualLinksTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+use Drupal\media\Entity\Media;
+
+/**
+ * Tests views contextual links on media items.
+ *
+ * @group media
+ */
+class MediaContextualLinksTest extends MediaJavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'contextual',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // This test is going to test the display, so we need the standalone URL.
+    \Drupal::configFactory()
+      ->getEditable('media.settings')
+      ->set('standalone_url', TRUE)
+      ->save(TRUE);
+
+    $this->container->get('router.builder')->rebuild();
+  }
+
+  /**
+   * Tests contextual links.
+   */
+  public function testMediaContextualLinks() {
+    // Create a media type.
+    $mediaType = $this->createMediaType('test');
+
+    // Create a media item.
+    $media = Media::create([
+      'bundle' => $mediaType->id(),
+      'name' => 'Unnamed',
+    ]);
+    $media->save();
+
+    $user = $this->drupalCreateUser([
+      'administer media',
+      'access contextual links',
+      'view media',
+    ]);
+    $this->drupalLogin($user);
+
+    $this->drupalGet('media/' . $media->id());
+
+    // Contextual links are populated by javascript after the page is loaded.
+    // Wait until they are on the page, click on the pencil so we make sure they
+    // are visible, and then we can assert their contents.
+    $this->assertSession()->waitForElement('css', 'div[data-contextual-id] ul.contextual-links');
+    $this->getSession()->executeScript("jQuery('.contextual .trigger').toggleClass('visually-hidden');");
+    $this->cssSelect('.contextual button')[0]->press();
+
+    // The contextual links container is there.
+    $this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'media:media=' . $media->id() . ':');
+
+    // The "Edit" link is there.
+    $this->assertSession()->elementTextContains('css', 'ul.contextual-links li:first-child a', 'Edit');
+
+    // The "Update metadata" link is there.
+    $this->assertSession()->elementTextContains('css', 'ul.contextual-links li:nth-child(2) a', 'Update metadata');
+
+    // The "Delete" link is there.
+    $this->assertSession()->elementTextContains('css', 'ul.contextual-links li:nth-child(3) a', 'Delete');
+  }
+
+}
diff --git a/core/modules/media/tests/src/Kernel/MediaUpdateMetadataTest.php b/core/modules/media/tests/src/Kernel/MediaUpdateMetadataTest.php
new file mode 100644
index 0000000000..aa6fc23af3
--- /dev/null
+++ b/core/modules/media/tests/src/Kernel/MediaUpdateMetadataTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\Tests\media\Kernel;
+
+use Drupal\media\Plugin\media\Source\File;
+
+/**
+ * Tests the update metadata method on Media entities.
+ *
+ * @group media
+ */
+class MediaUpdateMetadataTest extends MediaKernelTestBase {
+
+  /**
+   * Tests the update metadata operation.
+   */
+  public function testUpdateMetadata() {
+    $media_type = $this->createMediaType('file');
+    $source_plugin = $media_type->getSource();
+
+    // Initially the "name" metadata is the filename.
+    $media = $this->generateMedia('foo.txt', $media_type);
+    $media->save();
+    $name_metadata = $source_plugin->getMetadata($media, File::METADATA_ATTRIBUTE_NAME);
+    $this->assertSame('foo.txt', $name_metadata);
+
+    // Rename the file, simulating a remote metadata change.
+    $file_id = $source_plugin->getSourceFieldValue($media);
+    /** @var \Drupal\file\FileInterface $file */
+    $file = $this->container->get('entity_type.manager')
+      ->getStorage('file')
+      ->load($file_id);
+    $file->setFilename('bar.txt');
+    $file->save();
+
+    // Update metadata, we should now pick the new name.
+    $media->updateMetadata()->save();
+
+    $name_metadata = $source_plugin->getMetadata($media, File::METADATA_ATTRIBUTE_NAME);
+    $this->assertSame('bar.txt', $name_metadata);
+  }
+
+}
