Protected Sub Page_Load(sender As Object, e As EventArgs)
If Not Page.IsPostBack Then
ViewState("RowsCount") = 1
End If
End Sub

Protected Sub addRow(sender As Object, e As EventArgs) Handles Button1.Click
'adds the column row once we know the header row is there
If ViewState("RowsCount") IsNot Nothing Then
tbMaterials.Visible = True
numOfRows = Convert.ToInt32(ViewState("RowsCount").ToString)
GenerateTable(numOfRows)
End If
End Sub

Private Sub GenerateTable(rowsCount As Integer)
'Creat the Table and Add it to the Page

Dim table As Table = tbMaterials

'The number of Columns to be generated
Const colsCount As Integer = 5
'You can changed the value of 3 based on you requirements

' Now iterate through the table and add your controls
For i As Integer = 0 To rowsCount - 1
Dim row As New TableRow()

For j As Integer = 0 To colsCount - 1
Dim cell As New TableCell()
Dim tb As New TextBox()

' Set a unique ID for each TextBox added
tb.ID = "TextBoxRow_" + i.ToString + "Col_" + j.ToString
' Add the control to the TableCell
cell.Controls.Add(tb)
' Add the TableCell to the TableRow
row.Cells.Add(cell)
Next

Can you check using view source from browser if ID you are using to get form variable are the same as name of name of variable you are using in tb.Text = Request.Form("TextBoxRow_" + i.ToString + "Col_" + j.ToString). As we can not use id for request variables.