diff --git a/typo3/sysext/backend/Classes/Form/Container/OuterWrapContainer.php b/typo3/sysext/backend/Classes/Form/Container/OuterWrapContainer.php
index b3aed3cce2c49dfa2a235e34817723cdcaaa7a40..9aeb130bd1a0869ca9aa48853d2c45d577a947c1 100644
--- a/typo3/sysext/backend/Classes/Form/Container/OuterWrapContainer.php
+++ b/typo3/sysext/backend/Classes/Form/Container/OuterWrapContainer.php
@@ -92,7 +92,8 @@ class OuterWrapContainer extends AbstractContainer {
 
 			$newOrUid = ' <span class="typo3-TCEforms-recUid">[' . htmlspecialchars($row['uid']) . ']</span>';
 
-			$recordLabel = BackendUtility::getRecordTitle($table, FormEngineUtility::databaseRowCompatibility($row), TRUE, FALSE);
+			// @todo: getRecordTitlePrep applies an htmlspecialchars here
+			$recordLabel = BackendUtility::getRecordTitlePrep($this->data['recordTitle']);
 			if ($table === 'pages') {
 				$label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editPage', TRUE);
 				$pageTitle = sprintf($label, $tableTitle, $recordLabel);
@@ -100,13 +101,13 @@ class OuterWrapContainer extends AbstractContainer {
 				$label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecord', TRUE);
 				$workspacedPageRecord = BackendUtility::getRecordWSOL('pages', $row['pid'], 'uid,title');
 				$pageTitle = BackendUtility::getRecordTitle('pages', $workspacedPageRecord, TRUE, FALSE);
-				if ($recordLabel === BackendUtility::getNoRecordTitle(TRUE)) {
+				if (empty($recordLabel)) {
 					$label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordNoTitle', TRUE);
 				}
 				if ($this->data['effectivePid'] === 0) {
 					$label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordRootLevel', TRUE);
 				}
-				if ($recordLabel !== BackendUtility::getNoRecordTitle(TRUE)) {
+				if (!empty($recordLabel)) {
 					// Use record title and prepend an edit label.
 					$pageTitle = sprintf($label, $tableTitle, $recordLabel, $pageTitle);
 				} else {
diff --git a/typo3/sysext/backend/Classes/Form/FormDataCompiler.php b/typo3/sysext/backend/Classes/Form/FormDataCompiler.php
index 8f4f70dd87cbbbe7147d85594b8e5e0fba38d670..2c6c92776ea820188585b879d4dcc31bfc2a2b62 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataCompiler.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataCompiler.php
@@ -116,6 +116,8 @@ class FormDataCompiler {
 			'vanillaUid' => 0,
 			// Url to return to
 			'returnUrl' => NULL,
+			// Title of the handled record.
+			'recordTitle' => '',
 			// Parent page record is either the full row of the parent page the record is located at or should
 			// be added to, or it is NULL, if a record is added or edited below the root page node.
 			'parentPageRow' => NULL,
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php
new file mode 100644
index 0000000000000000000000000000000000000000..687232041e3087a00f857e45cc19922d7624eaa1
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php
@@ -0,0 +1,326 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\FormDataProvider;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Database\DatabaseConnection;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Determine the title of a record and write it to $result['recordTitle'].
+ *
+ * TCA ctrl fields like label and label_alt are evaluated and their
+ * current values from databaseRow used to create the title.
+ */
+class TcaRecordTitle implements FormDataProviderInterface {
+
+	/**
+	 * Enrich the processed record information with the resolved title
+	 *
+	 * @param array $result Incoming result array
+	 * @return array Modified array
+	 */
+	public function addData(array $result) {
+		if (!isset($result['processedTca']['ctrl']['label'])) {
+			throw new \UnexpectedValueException(
+				'TCA of table ' . $result['tableName'] . ' misses required [\'ctrl\'][\'label\'] definition.',
+				1443706103
+			);
+		}
+
+		if (isset($result['processedTca']['ctrl']['label_userFunc'])) {
+			// userFunc takes precedence over everything
+			$parameters = [
+				'table' => $result['tableName'],
+				'row' => $result['databaseRow'],
+				'title' => '',
+				'options' => isset($result['processedTca']['ctrl']['label_userFunc_options'])
+					? $result['processedTca']['ctrl']['label_userFunc_options']
+					: [],
+			];
+			$null = NULL;
+			GeneralUtility::callUserFunction($result['processedTca']['ctrl']['label_userFunc'], $parameters, $null);
+			$result['recordTitle'] = $parameters['title'];
+		} else {
+			$result = $this->getRecordTitleByLabelProperties($result);
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Build the record title from label, label_alt and label_alt_force properties
+	 *
+	 * @param array $result Incoming result array
+	 * @return array Modified result array
+	 */
+	protected function getRecordTitleByLabelProperties(array $result) {
+		$titles = [];
+		$titleByLabel = $this->getRecordTitleForField($result['processedTca']['ctrl']['label'], $result);
+		if (!empty($titleByLabel)) {
+			$titles[] = $titleByLabel;
+		}
+
+		$labelAltForce = isset($result['processedTca']['ctrl']['label_alt_force'])
+			? (bool)$result['processedTca']['ctrl']['label_alt_force']
+			: FALSE;
+		if (!empty($result['processedTca']['ctrl']['label_alt']) && ($labelAltForce || empty($titleByLabel))) {
+			// Dive into label_alt evaluation if label_alt_force is set or if label did not came up with a title yet
+			$labelAltFields = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['label_alt'], TRUE);
+			foreach ($labelAltFields as $fieldName) {
+				$titleByLabelAlt = $this->getRecordTitleForField($fieldName, $result);
+				if (!empty($titleByLabelAlt)) {
+					$titles[] = $titleByLabelAlt;
+				}
+				if (!$labelAltForce && !empty($titleByLabelAlt)) {
+					// label_alt_force creates a comma separated list of multiple fields.
+					// If not set, one found field with content is enough
+					break;
+				}
+			}
+		}
+
+		$result['recordTitle'] = implode(', ', $titles);
+		return $result;
+	}
+
+	/**
+	 * Record title of a single field
+	 *
+	 * @param string $fieldName Field to handle
+	 * @param array $result Incoming result array
+	 * @return string
+	 */
+	protected function getRecordTitleForField($fieldName, $result) {
+		if ($fieldName === 'uid') {
+			// uid return field content directly since it usually has not TCA definition
+			return $result['databaseRow']['uid'];
+		}
+
+		if (!isset($result['processedTca']['columns'][$fieldName]['config']['type'])
+			|| !is_string($result['processedTca']['columns'][$fieldName]['config']['type'])
+		) {
+			return '';
+		}
+
+		$recordTitle = '';
+		$rawValue = NULL;
+		if (array_key_exists($fieldName, $result['databaseRow'])) {
+			$rawValue = $result['databaseRow'][$fieldName];
+		}
+		$fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
+		switch ($fieldConfig['type']) {
+			case 'radio':
+				$recordTitle = $this->getRecordTitleForRadioType($rawValue, $fieldConfig);
+				break;
+			case 'inline':
+				// intentional fall-through
+			case 'select':
+				$recordTitle = $this->getRecordTitleForSelectType($rawValue, $fieldConfig);
+				break;
+			case 'group':
+				$recordTitle = $this->getRecordTitleForGroupType($rawValue, $fieldConfig);
+				break;
+			case 'check':
+				$recordTitle = $this->getRecordTitleForCheckboxType($rawValue, $fieldConfig);
+				break;
+			case 'input':
+				$recordTitle = $this->getRecordTitleForInputType($rawValue, $fieldConfig);
+				break;
+			case 'text':
+				$recordTitle = $this->getRecordTitleForTextType($rawValue);
+			case 'flex':
+				// TODO: Check if and how a label could be generated from flex field data
+			default:
+
+		}
+
+		return $recordTitle;
+	}
+
+	/**
+	 * Return the record title for radio fields
+	 *
+	 * @param mixed $value Current database value of this field
+	 * @param array $fieldConfig TCA field configuration
+	 * @return string
+	 */
+	protected function getRecordTitleForRadioType($value, $fieldConfig) {
+		if (!isset($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
+			return '';
+		}
+		foreach ($fieldConfig['items'] as $item) {
+			list($itemLabel, $itemValue) = $item;
+			if ((string)$value === (string)$itemValue) {
+				return $itemLabel;
+			}
+		}
+		return '';
+	}
+
+	/**
+	 * Return the record title for database records
+	 *
+	 * @param mixed $value Current database value of this field
+	 * @param array $fieldConfig TCA field configuration
+	 * @return string
+	 */
+	protected function getRecordTitleForSelectType($value, $fieldConfig) {
+		if (!is_array($value)) {
+			return '';
+		}
+		$labelParts = [];
+		foreach ($value as $itemValue) {
+			$itemKey = array_search($itemValue, array_column($fieldConfig['items'], 1));
+			if ($itemKey !== FALSE) {
+				$labelParts[] = $fieldConfig['items'][$itemKey][0];
+			}
+		}
+		$title = implode(', ', $labelParts);
+		if (empty($title) && !empty($value)) {
+			$title = implode(', ', $value);
+		}
+		return $title;
+	}
+
+	/**
+	 * Return the record title for database records
+	 *
+	 * @param mixed $value Current database value of this field
+	 * @param array $fieldConfig TCA field configuration
+	 * @return string
+	 */
+	protected function getRecordTitleForGroupType($value, $fieldConfig) {
+		if ($fieldConfig['internal_type'] !== 'db') {
+			return implode(', ', GeneralUtility::trimExplode(',', $value, TRUE));
+		}
+		$labelParts = array_map(
+			function($rawLabelItem) {
+				return array_pop(GeneralUtility::trimExplode('|', $rawLabelItem, TRUE, 2));
+			},
+			GeneralUtility::trimExplode(',', $value, TRUE)
+		);
+		if (!empty($labelParts)) {
+			sort($labelParts);
+			return implode(', ', $labelParts);
+		}
+		return '';
+	}
+
+	/**
+	 * Returns the record title for checkbox fields
+	 *
+	 * @param mixed $value Current database value of this field
+	 * @param array $fieldConfig TCA field configuration
+	 * @return string
+	 */
+	protected function getRecordTitleForCheckboxType($value, $fieldConfig) {
+		$languageService = $this->getLanguageService();
+		if (empty($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
+			$title = (bool)$value
+				? $languageService->sL('LLL:EXT:lang/locallang_common.xlf:yes')
+				: $languageService->sL('LLL:EXT:lang/locallang_common.xlf:no');
+		} else {
+			$labelParts = [];
+			foreach ($fieldConfig['items'] as $key => $val) {
+				if ($value & pow(2, $key)) {
+					$labelParts[] = $val[0];
+				}
+			}
+			$title = implode(', ', $labelParts);
+		}
+		return $title;
+	}
+
+	/**
+	 * Returns the record title for input fields
+	 *
+	 * @param mixed $value Current database value of this field
+	 * @param array $fieldConfig TCA field configuration
+	 * @return string
+	 */
+	protected function getRecordTitleForInputType($value, $fieldConfig) {
+		if (!isset($value)) {
+			return '';
+		}
+		$title = $value;
+		if (GeneralUtility::inList($fieldConfig['eval'], 'date')) {
+			if (isset($fieldConfig['dbType']) && $fieldConfig['dbType'] === 'date') {
+				$value = $value === '0000-00-00' ? 0 : (int)strtotime($value);
+			} else {
+				$value = (int)$value;
+			}
+			if (!empty($value)) {
+				$ageSuffix = '';
+				// Generate age suffix as long as not explicitly suppressed
+				if (!isset($fieldConfig['disableAgeDisplay']) || (bool)$fieldConfig['disableAgeDisplay'] === FALSE) {
+					$ageDelta = $GLOBALS['EXEC_TIME'] - $value;
+					$calculatedAge = BackendUtility::calcAge(
+						abs($ageDelta),
+						$this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')
+					);
+					$ageSuffix = ' (' . ($ageDelta > 0 ? '-' : '') . $calculatedAge . ')';
+				}
+				$title = BackendUtility::date($value) . $ageSuffix;
+			}
+		} elseif (GeneralUtility::inList($fieldConfig['eval'], 'time')) {
+			if (!empty($value)) {
+				$title = BackendUtility::time((int)$value, FALSE);
+			}
+		} elseif (GeneralUtility::inList($fieldConfig['eval'], 'timesec')) {
+			if (!empty($value)) {
+				$title = BackendUtility::time((int)$value);
+			}
+		} elseif (GeneralUtility::inList($fieldConfig['eval'], 'datetime')) {
+			// Handle native date/time field
+			if (isset($fieldConfig['dbType']) && $fieldConfig['dbType'] === 'datetime') {
+				$value = $value === '0000-00-00 00:00:00' ? 0 : (int)strtotime($value);
+			} else {
+				$value = (int)$value;
+			}
+			if (!empty($value)) {
+				$title = BackendUtility::datetime($value);
+			}
+		}
+		return $title;
+	}
+
+	/**
+	 * Returns the record title for text fields
+	 *
+	 * @param mixed $value Current database value of this field
+	 * @return string
+	 */
+	protected function getRecordTitleForTextType($value) {
+		return trim(strip_tags($value));
+	}
+
+	/**
+	 * @return DatabaseConnection
+	 */
+	protected function getDatabaseConnection() {
+		return $GLOBALS['TYPO3_DB'];
+	}
+
+	/**
+	 * @return LanguageService
+	 */
+	protected function getLanguageService() {
+		return $GLOBALS['LANG'];
+	}
+
+}
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaTypesRemoveUnusedColumns.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaTypesRemoveUnusedColumns.php
index 51b580bd2531bb0616e134cad3662831fdc73dc0..c0c6a730579cf9b94675686edfcd339223dc14a1 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaTypesRemoveUnusedColumns.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaTypesRemoveUnusedColumns.php
@@ -43,7 +43,16 @@ class TcaTypesRemoveUnusedColumns implements FormDataProviderInterface {
 
 		$showItemFieldString = $result['processedTca']['types'][$recordTypeValue]['showitem'];
 		$showItemFieldArray = GeneralUtility::trimExplode(',', $showItemFieldString, TRUE);
-		$shownColumnFields = [];
+
+		// Do not remove fields that are used for record title calculation
+		$shownColumnFields = empty($result['processedTca']['ctrl']['label']) ? [] : [$result['processedTca']['ctrl']['label']];
+		if (!empty($result['processedTca']['ctrl']['label_alt'])) {
+			$shownColumnFields = array_merge(
+				$shownColumnFields,
+				GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['label_alt'], TRUE)
+			);
+		}
+
 		foreach ($showItemFieldArray as $fieldConfigurationString) {
 			$fieldConfigurationArray = GeneralUtility::trimExplode(';', $fieldConfigurationString);
 			$fieldName = $fieldConfigurationArray[0];
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaRecordTitleTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaRecordTitleTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..93b290bbbdc598338d41771dc0d231484a38107a
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaRecordTitleTest.php
@@ -0,0 +1,796 @@
+<?php
+namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Backend\Form\FormDataProvider\TcaRecordTitle;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Test case
+ */
+class TcaRecordTitleTest extends UnitTestCase {
+
+	/**
+	 * @var TcaRecordTitle
+	 */
+	protected $subject;
+
+	/**
+	 * @var string
+	 */
+	protected $timeZone;
+
+	public function setUp() {
+		$this->subject = new TcaRecordTitle();
+		$this->timeZone = date_default_timezone_get();
+		date_default_timezone_set('UTC');
+	}
+
+	protected function tearDown() {
+		date_default_timezone_set($this->timeZone);
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataThrowsExceptionWithMissingLabel() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRew' => [],
+			'processedTca' => [
+				'ctrl' => [],
+			],
+		];
+		$this->setExpectedException(\UnexpectedValueException::class, $this->any(), 1443706103);
+		$this->subject->addData($input);
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleForLabelUserFunction() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'uid',
+					'label_userFunc' => function (&$parameters) {
+						$parameters['title'] = 'Test';
+					}
+				],
+				'columns' => [],
+			],
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'Test';
+
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleForUid() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => 'NEW56017ee37d10e587251374',
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'uid'
+				],
+				'columns' => [],
+			]
+		];
+
+		/** @var LanguageService|ObjectProphecy $languageService */
+		$languageService = $this->prophesize(LanguageService::class);
+		$GLOBALS['LANG'] = $languageService->reveal();
+		$languageService->sL(Argument::cetera())->willReturnArgument(0);
+
+		$expected = $input;
+		$expected['recordTitle'] = 'NEW56017ee37d10e587251374';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * Data provider for addDataReturnsRecordTitleForInputType
+	 * Each data set is an array with the following elements:
+	 *  - TCA field ['config'] section
+	 *  - Database value for field
+	 *  - expected title to be generated
+	 *
+	 * @returns array
+	 */
+	public function addDataReturnsRecordTitleForInputTypeDataProvider() {
+		return [
+			'new record' => [
+				[
+					'type' => 'input',
+				],
+				'',
+				'',
+			],
+			'plain text input' => [
+				[
+					'type' => 'input',
+				],
+				'aValue',
+				'aValue',
+			],
+			'date input' => [
+				[
+					'type' => 'input',
+					'eval' => 'date'
+				],
+				'978307261',
+				'01-01-01 (-7 days)',
+			],
+			'date input (dbType: date)' => [
+				[
+					'type' => 'input',
+					'eval' => 'date',
+					'dbType' => 'date'
+				],
+				'2001-01-01',
+				'01-01-01 (-7 days)',
+			],
+			'date input (disableAgeDisplay: TRUE)' => [
+				[
+					'type' => 'input',
+					'eval' => 'date',
+					'disableAgeDisplay' => TRUE
+				],
+				'978307261',
+				'01-01-01',
+			],
+			'time input' => [
+				[
+					'type' => 'input',
+					'eval' => 'time',
+				],
+				'44100',
+				'12:15',
+			],
+			'timesec input' => [
+				[
+					'type' => 'input',
+					'eval' => 'timesec',
+				],
+				'44130',
+				'12:15:30',
+			],
+			'datetime input' => [
+				[
+					'type' => 'input',
+					'eval' => 'datetime',
+					'dbType' => 'date'
+				],
+				'978307261',
+				'01-01-01 00:01',
+			],
+			'datetime input (dbType: datetime)' => [
+				[
+					'type' => 'input',
+					'eval' => 'datetime',
+					'dbType' => 'datetime'
+				],
+				'2014-12-31 23:59:59',
+				'31-12-14 23:59',
+			],
+		];
+	}
+
+	/**
+	 * @test
+	 * @dataProvider addDataReturnsRecordTitleForInputTypeDataProvider
+	 *
+	 * @param array $fieldConfig
+	 * @param string $fieldValue
+	 * @param string $expectedTitle
+	 */
+	public function addDataReturnsRecordTitleForInputType($fieldConfig, $fieldValue, $expectedTitle) {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => $fieldValue,
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => $fieldConfig,
+					]
+				],
+			]
+		];
+
+		/** @var LanguageService|ObjectProphecy $languageService */
+		$languageService = $this->prophesize(LanguageService::class);
+		$GLOBALS['LANG'] = $languageService->reveal();
+		$languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')
+			->willReturn(' min| hrs| days| yrs| min| hour| day| year');
+		$languageService->sL(Argument::cetera())->willReturnArgument(0);
+		$GLOBALS['EXEC_TIME'] = 978912061;
+
+		$expected = $input;
+		$expected['recordTitle'] = $expectedTitle;
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleWithAlternativeLabel() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => '',
+				'anotherField' => 'anotherValue',
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField',
+					'label_alt' => 'anotherField',
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'anotherField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					]
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'anotherValue';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleWithMultipleAlternativeLabels() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => '',
+				'anotherField' => '',
+				'additionalField' => 'additionalValue'
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField',
+					'label_alt' => 'anotherField,additionalField',
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'anotherField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'additionalField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'additionalValue';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleWithForcedAlternativeLabel() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => 'aField',
+				'anotherField' => 'anotherField'
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField',
+					'label_alt' => 'anotherField',
+					'label_alt_force' => TRUE,
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'anotherField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'aField, anotherField';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleWithMultipleForcedAlternativeLabels() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => 'aField',
+				'anotherField' => 'anotherField',
+				'additionalField' => 'additionalValue'
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField',
+					'label_alt' => 'anotherField,additionalField',
+					'label_alt_force' => TRUE,
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'anotherField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'additionalField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'aField, anotherField, additionalValue';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleIgnoresEmptyAlternativeLabels() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => 'aField',
+				'anotherField' => '',
+				'additionalField' => 'additionalValue'
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField',
+					'label_alt' => 'anotherField,additionalField',
+					'label_alt_force' => TRUE,
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'anotherField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+					'additionalField' => [
+						'config' => [
+							'type' => 'input'
+						]
+					],
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'aField, additionalValue';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleForRadioType() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => '2',
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'radio',
+							'items' => [
+								['foo', 1],
+								['bar', 2],
+								['baz', 3],
+							]
+						]
+					]
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'bar';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * Data provider for addDataReturnsRecordTitleForGroupType
+	 * Each data set is an array with the following elements:
+	 *  - TCA field configuration (merged with base config)
+	 *  - Database value for field
+	 *  - expected title to be generated
+	 *
+	 * @returns array
+	 */
+	public function addDataReturnsRecordTitleForGroupTypeDataProvider() {
+		return [
+			'new record' => [
+				[
+					'internal_type' => 'db',
+				],
+				'',
+				''
+			],
+			'internal_type: file' => [
+				[
+					'internal_type' => 'file',
+				],
+				'somePath/aFile.jpg,someOtherPath/anotherFile.png',
+				'somePath/aFile.jpg, someOtherPath/anotherFile.png',
+			],
+			'internal_type: db, single table, single record' => [
+				[
+					'internal_type' => 'db',
+					'allowed' => 'aTable'
+				],
+				'1|aValue',
+				'aValue',
+			],
+			'internal_type: db, single table, multiple records' => [
+				[
+					'internal_type' => 'db',
+					'allowed' => 'aTable'
+				],
+				'1|aValue,3|anotherValue',
+				'aValue, anotherValue',
+			],
+			'internal_type: db, multiple tables, single record' => [
+				[
+					'internal_type' => 'db',
+					'allowed' => 'aTable,anotherTable'
+				],
+				'anotherTable_1|anotherValue',
+				'anotherValue',
+			],
+			'internal_type: db, multiple tables, multiple records' => [
+				[
+					'internal_type' => 'db',
+					'allowed' => 'aTable,anotherTable'
+				],
+				'anotherTable_1|anotherValue,aTable_1|aValue',
+				'aValue, anotherValue',
+			],
+		];
+	}
+
+	/**
+	 * @test
+	 * @dataProvider addDataReturnsRecordTitleForGroupTypeDataProvider
+	 *
+	 * @param array $fieldConfig
+	 * @param string $fieldValue
+	 * @param string $expectedTitle
+	 */
+	public function addDataReturnsRecordTitleForGroupType($fieldConfig, $fieldValue, $expectedTitle) {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => $fieldValue,
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => array_merge(
+							[
+								'type' => 'group',
+							],
+							$fieldConfig
+						),
+					]
+				],
+			]
+		];
+
+		/** @var LanguageService|ObjectProphecy $languageService */
+		$languageService = $this->prophesize(LanguageService::class);
+		$GLOBALS['LANG'] = $languageService->reveal();
+		$languageService->sL(Argument::cetera())->willReturnArgument(0);
+
+		$expected = $input;
+		$expected['recordTitle'] = $expectedTitle;
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleForGroupTypeWithInternalTypeDb() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'uid' => '1',
+				'aField' => 'aTable_1|aValue,anotherTable_2|anotherValue',
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'group',
+							'internal_type' => 'db',
+							'allowed' => 'aTable,anotherTable',
+						]
+					]
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'aValue, anotherValue';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleForSingleCheckboxType() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'aField' => 1,
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'check',
+						]
+					]
+				],
+			]
+		];
+
+		/** @var LanguageService|ObjectProphecy $languageService */
+		$languageService = $this->prophesize(LanguageService::class);
+		$GLOBALS['LANG'] = $languageService->reveal();
+		$languageService->sL(Argument::cetera())->willReturnArgument(0)->shouldBeCalled();
+
+		$expected = $input;
+		$expected['recordTitle'] = 'LLL:EXT:lang/locallang_common.xlf:yes';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleForArrayCheckboxType() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'aField' => '5'
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'check',
+							'items' => [
+								['foo', ''],
+								['bar', ''],
+								['baz', ''],
+							]
+						]
+					]
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'foo, baz';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsEmptyRecordTitleForFlexType() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'aField' => [
+					'data' => [
+						'sDEF' => [
+							'lDEF' => [
+								'aFlexField' => [
+									'vDEF' => 'aFlexValue',
+								]
+							]
+						]
+					]
+				]
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'flex',
+							'ds' => [
+								'sheets' => [
+									'sDEF' => [
+										'ROOT' => [
+											'type' => 'array',
+											'el' => [
+												'aFlexField' => [
+													'label' => 'Some input field',
+													'config' => [
+														'type' => 'input',
+													],
+												],
+											],
+										],
+									],
+								],
+							]
+
+						]
+					]
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = '';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsRecordTitleForSelectType() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'aField' => [
+					'1',
+					'2'
+				]
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField'
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'select',
+							'items' => [
+								['foo', 1, NULL, NULL],
+								['bar', 2, NULL, NULL],
+								['baz', 4, NULL, NULL],
+							]
+						]
+					]
+				],
+			]
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'foo, bar';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataReturnsStrippedAndTrimmedValueForTextType() {
+		$input = [
+			'tableName' => 'aTable',
+			'databaseRow' => [
+				'aField' => '<p> text </p>',
+			],
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'aField',
+				],
+				'columns' => [
+					'aField' => [
+						'config' => [
+							'type' => 'text',
+						],
+					],
+				],
+			],
+		];
+
+		$expected = $input;
+		$expected['recordTitle'] = 'text';
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+}
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaTypesRemoveUnusedColumnsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaTypesRemoveUnusedColumnsTest.php
index bee73bc026ad76b106624fa3c778cdde3b45cbc9..8cbea5d3b6e729b4e192a96e98fbc44d76b3cf53 100644
--- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaTypesRemoveUnusedColumnsTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaTypesRemoveUnusedColumnsTest.php
@@ -81,7 +81,6 @@ class TcaTypesRemoveUnusedColumnsTest extends UnitTestCase {
 				'palettes' => [
 					'aPalette' => array(
 						'showitem' => 'keepMe',
-						'canNotCollapse' => TRUE
 					),
 				],
 				'columns' => [
@@ -121,7 +120,6 @@ class TcaTypesRemoveUnusedColumnsTest extends UnitTestCase {
 				'palettes' => [
 					'aPalette' => array(
 						'showitem' => 'aField',
-						'canNotCollapse' => TRUE
 					),
 				],
 				'columns' => [
@@ -150,4 +148,99 @@ class TcaTypesRemoveUnusedColumnsTest extends UnitTestCase {
 		$this->assertSame($expected, $this->subject->addData($input));
 	}
 
+	/**
+	 * @test
+	 */
+	public function addDataKeepsColumnsFieldReferencedInLabel() {
+		$input = [
+			'databaseRow' => [],
+			'recordTypeValue' => 'aType',
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'keepMe'
+				],
+				'types' => [
+					'aType' => [
+						'showitem' => 'keepMeToo'
+					],
+				],
+				'palettes' => [
+					'aPalette' => array(
+						'showitem' => 'keepMe',
+					),
+				],
+				'columns' => [
+					'keepMe' => [
+						'config' => [
+							'type' => 'input',
+						]
+					],
+					'keepMeToo' => [
+						'config' => [
+							'type' => 'input',
+						]
+					],
+					'aField' => [
+						'config' => [
+							'type' => 'input',
+						]
+					]
+				]
+			]
+		];
+
+		$expected = $input;
+		unset($expected['processedTca']['columns']['aField']);
+
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
+	/**
+	 * @test
+	 */
+	public function addDataKeepsColumnsFieldReferencedInLabelAlt() {
+		$input = [
+			'databaseRow' => [],
+			'recordTypeValue' => 'aType',
+			'processedTca' => [
+				'ctrl' => [
+					'label' => 'keepMe',
+					'label_alt' => 'keepMeToo'
+				],
+				'types' => [
+					'aType' => [
+						'showitem' => 'keepMe'
+					],
+				],
+				'palettes' => [
+					'aPalette' => array(
+						'showitem' => 'keepMe',
+					),
+				],
+				'columns' => [
+					'keepMe' => [
+						'config' => [
+							'type' => 'input',
+						]
+					],
+					'keepMeToo' => [
+						'config' => [
+							'type' => 'input',
+						]
+					],
+					'aField' => [
+						'config' => [
+							'type' => 'input',
+						]
+					]
+				]
+			]
+		];
+
+		$expected = $input;
+		unset($expected['processedTca']['columns']['aField']);
+
+		$this->assertSame($expected, $this->subject->addData($input));
+	}
+
 }
diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index 96a4917e4eab697b7a00134f8e3abc56cb113c7f..8ed9668be21db75b9137cdb56e05fa56eaba5e59 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -467,11 +467,16 @@ return array(
 							\TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class,
 						),
 					),
-					\TYPO3\CMS\Backend\Form\FormDataProvider\EvaluateDisplayConditions::class => array(
+					\TYPO3\CMS\Backend\Form\FormDataProvider\TcaRecordTitle::class => array(
 						'depends' => array(
 							\TYPO3\CMS\Backend\Form\FormDataProvider\TcaInline::class,
 						),
 					),
+					\TYPO3\CMS\Backend\Form\FormDataProvider\EvaluateDisplayConditions::class => array(
+						'depends' => array(
+							\TYPO3\CMS\Backend\Form\FormDataProvider\TcaRecordTitle::class,
+						),
+					),
 				),
 				'flexFormSegment' => array(
 					\TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultValues::class => array(),