diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index a52f76ab4109be95e96f5ea48bae2b19ba907ff7..a3454fc365321cb8dac2d6f141e87ec620cd57ad 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -397,7 +397,8 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
     $schema_handler = $this->database->schema();
 
     // Delete entity and field tables.
-    foreach ($this->getTableMapping($entity_type)->getTableNames() as $table_name) {
+    $table_names = $this->getTableNames($entity_type, $this->fieldStorageDefinitions, $this->getTableMapping($entity_type));
+    foreach ($table_names as $table_name) {
       if ($schema_handler->tableExists($table_name)) {
         $schema_handler->dropTable($table_name);
       }
@@ -472,7 +473,10 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E
 
     // Create temporary tables based on the new entity type and field storage
     // definitions.
-    $temporary_table_names = array_combine($sandbox['new_table_mapping']->getTableNames(), $sandbox['temporary_table_mapping']->getTableNames());
+    $temporary_table_names = array_combine(
+      $this->getTableNames($entity_type, $field_storage_definitions, $sandbox['new_table_mapping']),
+      $this->getTableNames($entity_type, $field_storage_definitions, $sandbox['temporary_table_mapping'])
+    );
     $this->entityType = $entity_type;
     $this->fieldStorageDefinitions = $field_storage_definitions;
 
@@ -512,14 +516,15 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E
     $this->storage->setEntityType($original);
     $this->storage->setFieldStorageDefinitions($original_field_storage_definitions);
     $this->storage->setTableMapping($sandbox['original_table_mapping']);
+
+    // Store the temporary table name mappings for later reuse.
+    $sandbox['temporary_table_names'] = $temporary_table_names;
   }
 
   /**
    * {@inheritdoc}
    */
   protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
-    /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $temporary_table_mapping */
-    $temporary_table_mapping = $sandbox['temporary_table_mapping'];
     /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $original_table_mapping */
     $original_table_mapping = $sandbox['original_table_mapping'];
     /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $new_table_mapping */
@@ -529,7 +534,10 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type,
 
     // Rename the original tables so we can put them back in place in case
     // anything goes wrong.
-    $backup_table_names = array_combine($original_table_mapping->getTableNames(), $backup_table_mapping->getTableNames());
+    $backup_table_names = array_combine(
+      $this->getTableNames($original, $original_field_storage_definitions, $original_table_mapping),
+      $this->getTableNames($original, $original_field_storage_definitions, $backup_table_mapping)
+    );
     $renamed_tables = [];
     try {
       foreach ($backup_table_names as $original_table_name => $backup_table_name) {
@@ -549,8 +557,7 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type,
     // Put the new tables in place and update the entity type and field storage
     // definitions.
     try {
-      $table_name_mapping = array_combine($temporary_table_mapping->getTableNames(), $new_table_mapping->getTableNames());
-      foreach ($table_name_mapping as $temp_table_name => $current_table_name) {
+      foreach ($sandbox['temporary_table_names'] as $current_table_name => $temp_table_name) {
         $this->database->schema()->renameTable($temp_table_name, $current_table_name);
       }
 
@@ -615,15 +622,50 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type,
     }
   }
 
+  /**
+   * Gets a list of table names for this entity type, field storage and mapping.
+   *
+   * The default table mapping does not return dedicated revision table names
+   * for non-revisionable fields attached to revisionable entity types. Since
+   * both the storage and the storage handlers expect them to be existing, the
+   * missing table names need to be manually restored.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   An entity type definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
+   *   An array of field storage definitions.
+   * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
+   *   A table mapping.
+   *
+   * @return string[]
+   *   An array of field table names.
+   *
+   * @todo Remove this once the behavior of the default table mapping, the
+   *    storage handler, and the storage schema handler are reconciled in
+   *    https://www.drupal.org/node/3113639.
+   */
+  private function getTableNames(EntityTypeInterface $entity_type, array $field_storage_definitions, TableMappingInterface $table_mapping) {
+    $table_names = $table_mapping->getTableNames();
+    if ($table_mapping instanceof DefaultTableMapping && $entity_type->isRevisionable()) {
+      foreach ($field_storage_definitions as $storage_definition) {
+        if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
+          $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
+          if (!$storage_definition->isRevisionable() && !in_array($dedicated_revision_table_name, $table_names)) {
+            $table_names[] = $dedicated_revision_table_name;
+          }
+        }
+      }
+    }
+    return $table_names;
+  }
+
   /**
    * {@inheritdoc}
    */
   protected function handleEntityTypeSchemaUpdateExceptionOnDataCopy(EntityTypeInterface $entity_type, EntityTypeInterface $original, array &$sandbox) {
     // In case of an error during the save process, we need to clean up the
     // temporary tables.
-    /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $temporary_table_mapping */
-    $temporary_table_mapping = $sandbox['temporary_table_mapping'];
-    foreach ($temporary_table_mapping->getTableNames() as $table_name) {
+    foreach ($sandbox['temporary_table_names'] as $table_name) {
       $this->database->schema()->dropTable($table_name);
     }
   }
diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php
index d2ffcc4caeb0ad9691a31dc0ba583b550390d2dd..6c1d38387e8f50bcc68a86a56f5c74b7f0ac9d3d 100644
--- a/core/lib/Drupal/Core/Entity/entity.api.php
+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -1828,6 +1828,11 @@ function hook_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDi
 /**
  * Provides custom base field definitions for a content entity type.
  *
+ * Field (storage) definitions returned by this hook must run through the
+ * regular field storage life-cycle operations: they need to be properly
+ * installed, updated, and uninstalled. This would typically be done through the
+ * Entity Update API provided by the entity definition update manager.
+ *
  * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  *   The entity type definition.
  *
@@ -1839,6 +1844,8 @@ function hook_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDi
  * @see hook_entity_bundle_field_info_alter()
  * @see \Drupal\Core\Field\FieldDefinitionInterface
  * @see \Drupal\Core\Entity\EntityFieldManagerInterface::getFieldDefinitions()
+ * @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+ * @see https://www.drupal.org/node/3034742
  */
 function hook_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
   if ($entity_type->id() == 'node') {
@@ -1943,6 +1950,11 @@ function hook_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\Entit
 /**
  * Provides field storage definitions for a content entity type.
  *
+ * Field storage definitions returned by this hook must run through the regular
+ * field storage life-cycle operations: they need to be properly installed,
+ * updated, and uninstalled. This would typically be done through the Entity
+ * Update API provided by the entity definition update manager.
+ *
  * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  *   The entity type definition.
  *
@@ -1952,6 +1964,8 @@ function hook_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\Entit
  * @see hook_entity_field_storage_info_alter()
  * @see \Drupal\Core\Field\FieldStorageDefinitionInterface
  * @see \Drupal\Core\Entity\EntityFieldManagerInterface::getFieldStorageDefinitions()
+ * @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+ * @see https://www.drupal.org/node/3034742
  */
 function hook_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
   if (\Drupal::entityTypeManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
diff --git a/core/modules/system/tests/src/Functional/Entity/Traits/EntityDefinitionTestTrait.php b/core/modules/system/tests/src/Functional/Entity/Traits/EntityDefinitionTestTrait.php
index 1f4cff34436c6310139de45a6863de911db4dbd4..7d0b2ecce36b4b48af018ff29219ed77fa069d20 100644
--- a/core/modules/system/tests/src/Functional/Entity/Traits/EntityDefinitionTestTrait.php
+++ b/core/modules/system/tests/src/Functional/Entity/Traits/EntityDefinitionTestTrait.php
@@ -318,12 +318,18 @@ protected function removeBaseFieldIndex() {
    *
    * @param string $type
    *   (optional) The field type for the new field. Defaults to 'string'.
+   * @param bool $revisionable
+   *   (optional) Whether the field should be revisionable. Defaults to FALSE.
+   * @param bool $translatable
+   *   (optional) Whether the field should be translatable. Defaults to FALSE.
    */
-  protected function addBundleField($type = 'string') {
+  protected function addBundleField($type = 'string', $revisionable = FALSE, $translatable = FALSE) {
     $definitions['new_bundle_field'] = FieldStorageDefinition::create($type)
       ->setName('new_bundle_field')
       ->setLabel(t('A new bundle field'))
-      ->setTargetEntityTypeId('entity_test_update');
+      ->setTargetEntityTypeId('entity_test_update')
+      ->setRevisionable($revisionable)
+      ->setTranslatable($translatable);
     $this->state->set('entity_test_update.additional_field_storage_definitions', $definitions);
     $this->state->set('entity_test_update.additional_bundle_field_definitions.test_bundle', $definitions);
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php
index a525f650eb31e12eef46b3fbae2e4490f06d0b1e..6503b41d2363141ddde494530bebd02de6bd540f 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php
@@ -94,6 +94,10 @@ protected function setUp() {
     $this->entityFieldManager = $this->container->get('entity_field.manager');
     $this->database = $this->container->get('database');
 
+    // Add a non-revisionable bundle field to test revision field table
+    // handling.
+    $this->addBundleField('string', FALSE, TRUE);
+
     // The 'changed' field type has a special behavior because it updates itself
     // automatically if any of the other field values of an entity have been
     // updated, so add it to the entity type that is being tested in order to
@@ -294,7 +298,9 @@ protected function insertData($revisionable, $translatable) {
     for ($i = $next_id; $i <= $next_id + 2; $i++) {
       $entity = $storage->create([
         'id' => $i,
+        'type' => 'test_bundle',
         'name' => 'test entity - ' . $i . ' - en',
+        'new_bundle_field' => 'bundle field - ' . $i . ' - en',
         'test_multiple_properties' => [
           'value1' => 'shared table - ' . $i . ' - value 1 - en',
           'value2' => 'shared table - ' . $i . ' - value 2 - en',
@@ -315,6 +321,7 @@ protected function insertData($revisionable, $translatable) {
       if ($translatable) {
         $translation = $entity->addTranslation('ro', [
           'name' => 'test entity - ' . $i . ' - ro',
+          'new_bundle_field' => 'bundle field - ' . $i . ' - ro',
           'test_multiple_properties' => [
             'value1' => 'shared table - ' . $i . ' - value 1 - ro',
             'value2' => 'shared table - ' . $i . ' - value 2 - ro',
@@ -338,6 +345,7 @@ protected function insertData($revisionable, $translatable) {
         // Create a new pending revision.
         $revision_2 = $storage->createRevision($entity, FALSE);
         $revision_2->name = 'test entity - ' . $i . ' - en - rev2';
+        $revision_2->new_bundle_field = 'bundle field - ' . $i . ' - en - rev2';
         $revision_2->test_multiple_properties->value1 = 'shared table - ' . $i . ' - value 1 - en - rev2';
         $revision_2->test_multiple_properties->value2 = 'shared table - ' . $i . ' - value 2 - en - rev2';
         $revision_2->test_multiple_properties_multiple_values[0]->value1 = 'dedicated table - ' . $i . ' - delta 0 - value 1 - en - rev2';
@@ -349,6 +357,7 @@ protected function insertData($revisionable, $translatable) {
         if ($translatable) {
           $revision_2_translation = $storage->createRevision($entity->getTranslation('ro'), FALSE);
           $revision_2_translation->name = 'test entity - ' . $i . ' - ro - rev2';
+          $revision_2->new_bundle_field = 'bundle field - ' . $i . ' - ro - rev2';
           $revision_2->test_multiple_properties->value1 = 'shared table - ' . $i . ' - value 1 - ro - rev2';
           $revision_2->test_multiple_properties->value2 = 'shared table - ' . $i . ' - value 2 - ro - rev2';
           $revision_2_translation->test_multiple_properties_multiple_values[0]->value1 = 'dedicated table - ' . $i . ' - delta 0 - value 1 - ro - rev2';
@@ -375,6 +384,7 @@ protected function assertEntityData($revisionable, $translatable) {
     foreach ($entities as $entity_id => $entity) {
       /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
       $this->assertEquals("test entity - {$entity->id()} - en", $entity->label());
+      $this->assertEquals("bundle field - {$entity->id()} - en", $entity->new_bundle_field->value);
       $this->assertEquals("shared table - {$entity->id()} - value 1 - en", $entity->test_multiple_properties->value1);
       $this->assertEquals("shared table - {$entity->id()} - value 2 - en", $entity->test_multiple_properties->value2);
       $this->assertEquals("dedicated table - {$entity->id()} - delta 0 - value 1 - en", $entity->test_multiple_properties_multiple_values[0]->value1);
@@ -385,6 +395,7 @@ protected function assertEntityData($revisionable, $translatable) {
       if ($translatable) {
         $translation = $entity->getTranslation('ro');
         $this->assertEquals("test entity - {$translation->id()} - ro", $translation->label());
+        $this->assertEquals("bundle field - {$entity->id()} - ro", $translation->new_bundle_field->value);
         $this->assertEquals("shared table - {$translation->id()} - value 1 - ro", $translation->test_multiple_properties->value1);
         $this->assertEquals("shared table - {$translation->id()} - value 2 - ro", $translation->test_multiple_properties->value2);
         $this->assertEquals("dedicated table - {$translation->id()} - delta 0 - value 1 - ro", $translation->test_multiple_properties_multiple_values[0]->value1);
@@ -403,6 +414,7 @@ protected function assertEntityData($revisionable, $translatable) {
         /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
         $revision_label = $revision->isDefaultRevision() ? NULL : ' - rev2';
         $this->assertEquals("test entity - {$revision->id()} - en{$revision_label}", $revision->label());
+        $this->assertEquals("bundle field - {$revision->id()} - en{$revision_label}", $revision->new_bundle_field->value);
         $this->assertEquals("shared table - {$revision->id()} - value 1 - en{$revision_label}", $revision->test_multiple_properties->value1);
         $this->assertEquals("shared table - {$revision->id()} - value 2 - en{$revision_label}", $revision->test_multiple_properties->value2);
         $this->assertEquals("dedicated table - {$revision->id()} - delta 0 - value 1 - en{$revision_label}", $revision->test_multiple_properties_multiple_values[0]->value1);
@@ -413,6 +425,7 @@ protected function assertEntityData($revisionable, $translatable) {
         if ($translatable) {
           $translation = $revision->getTranslation('ro');
           $this->assertEquals("test entity - {$translation->id()} - ro{$revision_label}", $translation->label());
+          $this->assertEquals("bundle field - {$entity->id()} - ro{$revision_label}", $translation->new_bundle_field->value);
           $this->assertEquals("shared table - {$revision->id()} - value 1 - ro{$revision_label}", $translation->test_multiple_properties->value1);
           $this->assertEquals("shared table - {$revision->id()} - value 2 - ro{$revision_label}", $translation->test_multiple_properties->value2);
           $this->assertEquals("dedicated table - {$translation->id()} - delta 0 - value 1 - ro{$revision_label}", $translation->test_multiple_properties_multiple_values[0]->value1);
@@ -457,6 +470,8 @@ protected function assertEntityTypeSchema($revisionable, $translatable, $new_bas
     else {
       $this->assertNonRevisionableAndNonTranslatable();
     }
+
+    $this->assertBundleFieldSchema($revisionable);
   }
 
   /**
@@ -589,6 +604,26 @@ protected function assertNonRevisionableAndNonTranslatable() {
     $this->assertFalse($database_schema->tableExists($entity_type->getRevisionDataTable()));
   }
 
+  /**
+   * Asserts that the bundle field schema is correct.
+   *
+   * @param bool $revisionable
+   *   Whether the entity type is revisionable or not.
+   */
+  protected function assertBundleFieldSchema($revisionable) {
+    $entity_type_id = 'entity_test_update';
+    $field_storage_definition = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id)['new_bundle_field'];
+    $database_schema = $this->database->schema();
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $this->entityTypeManager
+      ->getStorage($entity_type_id)
+      ->getTableMapping();
+    $this->assertTrue($database_schema->tableExists($table_mapping->getDedicatedDataTableName($field_storage_definition)));
+    if ($revisionable) {
+      $this->assertTrue($database_schema->tableExists($table_mapping->getDedicatedRevisionTableName($field_storage_definition)));
+    }
+  }
+
   /**
    * Asserts that the backup tables have been kept after a successful update.
    */
@@ -617,7 +652,7 @@ public function testFieldableEntityTypeUpdatesErrorHandling() {
     $this->insertData(FALSE, TRUE);
 
     $tables = $schema->findTables('old_%');
-    $this->assertCount(3, $tables);
+    $this->assertCount(4, $tables);
     foreach ($tables as $table) {
       $schema->dropTable($table);
     }
@@ -783,7 +818,7 @@ public function testFieldableEntityTypeUpdatesRemoveBackupTables() {
 
     // Check that backup tables are kept by default.
     $tables = $schema->findTables('old_%');
-    $this->assertCount(3, $tables);
+    $this->assertCount(4, $tables);
     foreach ($tables as $table) {
       $schema->dropTable($table);
     }